package org.rectask.model;

public class Date implements Comparable<Date> {
    public static final int JANUARY   = 1;
    public static final int FEBRUARY  = 2;
    public static final int MARCH     = 3;
    public static final int APRIL     = 4;
    public static final int MAY       = 5;
    public static final int JUNE      = 6;
    public static final int JULY      = 7;
    public static final int AUGUST    = 8;
    public static final int SEPTEMBER = 9;
    public static final int OCTOBER   = 10;
    public static final int NOVEMBER  = 11;
    public static final int DECEMBER  = 12;
    
    /**
     * The year
     */
    private int year;
    
    /**
     * The month
     */
    private int month;
    
    /**
     * The day
     */
    private int day;

    /**
     * A new date
     */
    public Date(int day, int month, int year) {
	if (!validDay(day, month, year))
	    throw new IllegalArgumentException("Invalid date");
	
	this.day = day;
	this.month = month;
	this.year = year;
    }
    
    /**
     * A new date
     */
    public Date(Date date) {
	this.day = date.day;
	this.month = date.month;
	this.year = date.year;
    }
    
    /**
     * 
     */
    static boolean validDay(int day, int month, int year){
	if (day < 1)
	    return false;
	
	if (year == 0) 
	    return false;
	
	switch (month) {
	case JANUARY:;
	case MARCH:;
	case MAY:;
	case JULY:;
	case OCTOBER:;
	case DECEMBER:
	    if (day < 32)
		return true;
	    break;
	case APRIL:;
	case JUNE:;
	case SEPTEMBER:;
	case NOVEMBER:
	    if (day < 31)
		return true;
	    break;
	case FEBRUARY:
	    if (isLeapYear(year)) {
		if (day < 30)
		    return true;
	    } else { 
		if (day < 29)
		    return true;
	    }	    
	    break;
	default:
	    return false;
	}
	return false;
    }
    
    /**
     *  
     * @param date
     * @return
     */
    public int daysUntil(Date date) {
	if (date == null)
	    return Integer.MAX_VALUE;
	
	if (this.after(date))
	    return 0 - date.daysUntil(this);
	    
	if (this.equals(date))
	    return 0;

	int result = 0;
	if (this.year < date.year) {
	    result += daysLeftInYearFrom(this);
	    result += 365 * (date.year - this.year - 1);
	    result += leapYearsStrictlyInBetween(this.year, date.year);
	    result += daysInYearUntil(date);
	} else {
	    result += daysInYearUntil(date);
	    result -= daysInYearUntil(this);
	}
	return result;
    }

    public static int daysInYearUntil(Date date) {
	return daysInYearUntil(date.day, date.month, date.year);
    }
    
    // TODO more efficiently, if needed (start here)
    public static int daysInYearUntil(int day, int month, int year) {
	int result = 0;
	if (month == JANUARY)
	    return day;
	result += 31;
	if (month == FEBRUARY)
	    return result += day;
	if (isLeapYear(year))
	    result += 29;
	else 
	    result += 28;
	if (month == MARCH)
	    return result += day;
	result += 31;
	if (month == APRIL)
	    return result += day;
	result += 30;
	if (month == MAY)
	    return result += day;
	result += 31;
	if (month == JUNE)
	    return result += day;
	result += 30;
	if (month == JULY)
	    return result += day;
	result += 31;
	if (month == AUGUST)
	    return result += day;
	result += 31;
	if (month == SEPTEMBER)
	    return result += day;
	result += 30;
	if (month == OCTOBER)
	    return result += day;
	result += 31;
	if (month == NOVEMBER)
	    return result += day;
	result += 30;
	if (month == DECEMBER)
	    return result += day;
	throw new IllegalArgumentException("Bad month");
    }
    
    public static int daysLeftInYearFrom(Date date) {
	return daysLeftInYearFrom(date.day, date.month, date.year);
    }
    
    // TODO more efficiently, if needed (start at daysInYearUntil())
    public static int daysLeftInYearFrom(int day, int month, int year) {
	int result = 0;
	if (month == DECEMBER) {
	    return result + 31 - day;
	}
	result += 31;
	if (month == NOVEMBER) {
	    return result + 30 - day;
	}
	result += 30;
	if (month == OCTOBER) {
	    return result + 31 - day;
	}
	result += 31;
	if (month == SEPTEMBER) {
	    return result + 30 - day;
	}
	result += 30;
	if (month == AUGUST) {
	    return result + 31 - day;
	}
	result += 31;
	if (month == JULY) {
	    return result + 31 - day;
	}
	result += 31;
	if (month == JUNE) {
	    return result + 30 - day;
	}
	result += 30;
	if (month == MAY) {
	    return result + 31 - day;
	}
	result += 31;
	if (month == APRIL) {
	    return result + 30 - day;
	}
	result += 30;
	if (month == MARCH) {
	    return result + 31 - day;
	}
	result += 31;
	if (month == FEBRUARY) {
	    if (isLeapYear(year)) {
		return result + 29 - day;
	    } else {
		return result + 28 - day;
	    }
	}
	if (isLeapYear(year)) {
	    result += 29;
	} else {
	    result += 28;
	}
	if (month == JANUARY) {
	    return result + 31 - day;
	}
	throw new IllegalArgumentException("Bad month");
    }
    
    /**
     * ...including neither.
     * 
     * @param formerYear
     * @param latterYear
     */
    public static int leapYearsStrictlyInBetween(int formerYear, int latterYear) {
	if (formerYear >= latterYear)
	    return 0;
	
	// TODO: make this more efficient, if needed
	int result = 0;
	for (int y = formerYear + 1; y < latterYear; y++) {
	    if (isLeapYear(y))
		result++;
	}
	return result;
    }
    
    public static boolean isLeapYear(int year) {
	if (year % 4 == 0){
	    if (year % 100 == 0) {
		if (year % 400 == 0) {
		    return true;
		} else {
		    return false;
		}
	    } else {
		return true;
	    }
	} else {
	    return false;
	}
	    
    }

    /** 
     * Add n days to this date.
     * 
     * @param n
     */
    public void addDays(int n) {
	if (n < 0)
	    throw new IllegalArgumentException("Negative argument.");
	
	while (day + n > maxDaysIn(year, month)) {
	    int restOfMonth = maxDaysIn(year, month) - day;
	    n -= restOfMonth;
	    day = 1;
	    if (month == DECEMBER) {
		month = JANUARY;
		year++;
	    } else {
		month++;
	    }
	}
	day += n;
    }

    /**
     * Returns the max number of days for the given month and year.
     * 
     * @param year
     * @param month
     * @return
     */
    public static int maxDaysIn(int year, int month) {
	switch (month) {
	case JANUARY:;
	case MARCH:;
	case MAY:;
	case JULY:;
	case OCTOBER:;
	case DECEMBER:
	    return 31;
	case APRIL:;
	case JUNE:;
	case SEPTEMBER:;
	case NOVEMBER:
	    return 30;
	case FEBRUARY:
	    if (isLeapYear(year)) {
		return 29;
	    } else { 
		return 28;
	    }	    
	default:
	    throw new IllegalArgumentException("Invalid month");
	}	
    }
    
    /**
     * 
     */
    public void addMonths(int n){
	if (n < 0)
	    throw new IllegalArgumentException("Negative argument.");
	
	while (month + n > 12) {
	    int restOfYear = 12 - month;
	    n -= restOfYear;
	    month = 1;
	    year++;
	}
	month += n;
    }
    
    /**
     * 
     */
    public void addYears(int n) {
	this.year += n;
    }

    /**
     * Returns a number that allows date ordering. Former dates
     * get lower numbers that future dates. 
     * 
     * @return a number that allows date ordering.
     */
    private int orderCode() {
	return 400 * year + Date.daysInYearUntil(this);
    }
    
    /**
     * Returns true if the other date is after this date
     */
    public boolean before(Date date) {
	if (this.orderCode() < date.orderCode())
	    return true;
	else
	    return false;
    }
    
    /**
     * Returns true if the other date is before this date
     */
    public boolean after(Date date) {
	if (this.orderCode() > date.orderCode())
	    return true;
	else
	    return false;
    }
    
    /**
     * 
     */
    public int compareTo(Date date) {
	if (this.before(date))
	    return -1;
	else if (this.after(date)) 
	    return 1;
	else 
	    return 0;
    }	

    /**
     * 
     */
    public boolean equals(Object o) {
	if (o instanceof Date) {
	    Date date = (Date) o;
	    if (this.year == date.year) {
		if (this.month == date.month)
		    if (this.day == date.day)
			return true;
	    }
	}
	return false;
    }

    /**
     * 
     */
    public int hashCode() {
	return 10000 * year + 100 + month + day;
    }
    
    /** 
     * Returns this date as a String YYYY-MM-DD (old ISO 2014)
     * 
     * @return
     */
    public String toString() {
	String result = "";
	result += this.year + "-";
	if (this.month < OCTOBER)
	    result += "0" + this.month;
	else
	    result += this.month;	    
	result += "-";
	if (this.day < 10) 
	    result += "0" + this.day;
	else
	    result += this.day;
	return result;
    }
    
    /**
     * Get a date YYYY-MM-DD (old ISO 2014) and return the 
     * corresponding Date.
     * 
     * @param date The date to be parsed
     * 
     * @return
     */
    public static Date parseDate(String date) {
	String[] token = date.split("-");
	if (token.length != 3)
	    return null;
	
	int day   = Integer.parseInt(token[2]);
	int month = Integer.parseInt(token[1]);
	int year  = Integer.parseInt(token[0]);
	
	Date result = new Date(day,month,year);
	return result;	
    }
    
    /**
     * @param args
     */
    public static void main(String[] args) {
	if (args.length != 2) {
	    System.out.println("USAGE: Date <date1> <date2>");
	    System.exit(0);
	}
	Date date1 = null;
	Date date2 = null;
	try {
	    date1 = Date.parseDate(args[0]);
	    date2 = Date.parseDate(args[1]);
	} catch (IllegalArgumentException e) {
	    System.out.println("Invalid date");
	    System.exit(-1);
	}
	if (date1 != null && date2 != null) {
	    System.out.print("Distance from " + date1 + " to " + date2);
	    System.out.println(" is " + date1.daysUntil(date2));
	} else{
	    System.out.println("Bad date format (bad number of '-'?).");
	}
    }

}
